import { Line, Rect, Text } from '@newcar/basic'
import type { WidgetStyle } from '@newcar/core'
import type { Canvas, CanvasKit, Paint } from 'canvaskit-wasm'
import { Color } from '@newcar/utils'
import stringWidth from 'string-width'
import type { DateTimeUnit } from 'luxon'
import { DateTime } from 'luxon'
import type { BaseChartData, BaseChartOptions, DateTimeFormatOptions } from './baseChart'
import { BaseChart } from './baseChart'

/**
 * Chart Layout Options
 * @category General
 * @extends BaseChartOptions
 */
export interface ChartLayoutOptions extends BaseChartOptions {
}

/**
 * Chart Layout Style
 * @category General
 * @extends WidgetStyle
 */
export interface ChartLayoutStyle extends WidgetStyle {
}

/**
 * Chart Axis
 * @category General
 */
interface ChartAxis {
  /**
   * Suggested minimum value.
   */
  suggestedMin: number | DateTime
  /**
   * Suggested maximum value.
   */
  suggestedMax: number | DateTime
  /**
   * Grid color.
   */
  gridColor: Color
  /**
   * Grid width.
   */
  gridWidth: number
  /**
   * Minimum value. Generated by the data and the suggested minimum value.
   */
  min: number
  /**
   * Maximum value. Generated by the data and the suggested maximum value.
   */
  max: number
  /**
   * Interval. Generated by the data.
   */
  interval: number
  /**
   * Positions of the axis labels.
   */
  pos: number[]
  /**
   * Positions of the grid lines and the ticks.
   */
  posLine?: number[]
  /**
   * Label texts.
   */
  labelTexts?: string[]
  /**
   * Axis line.
   */
  axis?: Line
  /**
   * Ticks.
   */
  ticks: Line[]
  /**
   * Grid lines.
   */
  grids: Line[]
  /**
   * Labels.
   */
  labels: Text[]
}

/**
 * Chart Layout
 * @category General
 * @extends BaseChart
 * @description
 * Chart layout widget. It provides the layout for the chart. It includes the axis, the grid, and the legend.
 * It should not be used directly. It is used by the chart widgets.
 */
export class ChartLayout extends BaseChart {
  /**
   * The style of the chart layout.
   * @public
   * @type ChartLayoutStyle
   */
  declare style: ChartLayoutStyle
  /**
   * The size of the chart layout.
   * @public
   * @type { width: number, height: number }
   * @default { width: 300, height: 300 }
   * @description
   * The size of the chart layout.
   */
  size: {
    /**
     * The width of the chart layout.
     * @public
     * @type number
     * @default 300
     * @description
     * The width of the chart layout.
     * To be noted that the width is the width of chart layout (excluding label, legend, etc.).
     */
    width: number
    /**
     * The height of the chart layout.
     * @public
     * @type number
     * @default 300
     * @description
     * The height of the chart layout.
     * To be noted that the height is the height of chart layout (excluding label, legend, etc.).
     */
    height: number
  }

  /**
   * The index-axis of the chart.
   * @public
   * @type 'x' | 'y'
   * @default 'x'
   * @description
   * The index-axis of the chart.
   */
  indexAxis: 'x' | 'y'
  /**
   * The type of the index-axis.
   * @public
   * @type 'label' | 'number' | 'date'
   * @default 'label'
   * @description
   * {@link BaseChartOptions#indexType}
   */
  indexType: 'label' | 'number' | 'date'
  /**
   * The interval unit of the index-axis.
   * @public
   * @type DateTimeUnit
   * @description
   * {@link BaseChartOptions#indexIntervalUnit}
   */
  indexIntervalUnit?: DateTimeUnit
  /**
   * The date format options of the index-axis.
   * @public
   * @type DateTimeFormatOptions
   * @description
   * {@link BaseChartOptions#dateFormatOptions}
   */
  dateFormatOptions?: DateTimeFormatOptions
  /**
   * Whether the grid aligns with the index-axis.
   * @public
   * @type boolean
   * @default true
   * @description
   * Whether the grid aligns with the index-axis.
   */
  gridAlign: boolean
  /**
   * Whether the grid aligns with the edge.
   * @public
   * @type boolean
   * @default false
   * @description
   * Whether the grid aligns with the edge.
   */
  edgeOffset: boolean

  /**
   * The paint of the chart layout.
   */
  paint: Paint
  /**
   * The legends of the chart.
   */
  legends: Rect[]
  /**
   * The legend labels of the chart.
   */
  legendLabels: Text[]

  /**
   * The index-axis of the chart.
   */
  index: ChartAxis
  /**
   * The cross-axis of the chart.
   */
  cross: ChartAxis

  /**
   * Create a chart layout.
   * @public
   * @constructor
   * @param data - The data of the chart.
   * @param options - The options of the chart.
   */
  constructor(public data: BaseChartData, options?: ChartLayoutOptions) {
    options ??= {}
    super(options)
    this.size = options.size ?? { width: 300, height: 300 }
    this.indexAxis = options.indexAxis ?? 'x'
    this.indexType = options.indexType
    ?? (data.labels && data.labels.every(label => (typeof label === 'string')))
      ? 'label'
      : (data.labels && data.labels.every(label => (label instanceof DateTime)))
      || (data.datasets.flatMap(set => (set.data.map(unit => (unit.isIndexDate())))).every(isDate => (isDate)))
          ? 'date'
          : 'number'
    this.indexIntervalUnit = options.indexIntervalUnit
    this.dateFormatOptions = {
      year: 'year',
      quarter: 'quarter',
      month: 'monthLong',
      week: 'weekNumber',
      day: 'weekdayLong',
      hour: 'hour',
      minute: 'minute',
      second: 'second',
      millisecond: 'millisecond',
      ...options.dateFormatOptions,
    }
    this.gridAlign = options.gridAlign ?? true
    this.edgeOffset = options.edgeOffset ?? false

    // indexType validation
    if (this.indexType === 'label' && !data.labels)
      throw new Error('indexType is label but labels are not provided')
    if (this.indexType === 'date' && data.datasets.flatMap(set => (set.data.map(unit => (unit.isIndexDate())))).some(isDate => (!isDate)))
      throw new Error('indexType is date but some index/labels are not DateTimes')
    if (this.indexType === 'number' && data.datasets.flatMap(set => (set.data.map(unit => (unit.isIndexDate())))).some(isDate => (isDate)))
      throw new Error('indexType is number but some index are DateTimes')

    this.index = {
      suggestedMin: options.axis?.index?.suggestedMin
      ?? options.suggestedMin
      ?? (options.axis?.index?.beginAtZero ?? true)
        ? 0
        : Number.POSITIVE_INFINITY,
      suggestedMax: options.axis?.index?.suggestedMax
      ?? options.suggestedMax
      ?? Number.NEGATIVE_INFINITY,
      gridColor: options.axis?.index?.gridColor ?? options.gridColor ?? Color.WHITE,
      gridWidth: options.axis?.index?.gridWidth ?? options.gridWidth ?? 1,
      min: 0,
      max: 0,
      interval: 0,
      pos: [],
      posLine: [],
      labelTexts: [],
      ticks: [],
      grids: [],
      labels: [],
    }

    this.index.axis = new Line(
      [0, 0],
      [0, this.size.height],
      {
        style: {
          color: this.index.gridColor,
          width: this.index.gridWidth,
          transparency: 0.6,
        },
      },
    )

    if (this.indexType === 'label') {
      this.index.min = 0
      this.index.max = data.labels.length - 1
      this.index.interval = 1
      for (let i = 0; i < data.labels.length; i++)
        this.index.pos.push(i)
      this.index.labelTexts = data.labels.map(label => (label.toString()))
    }
    else if (this.indexType === 'date') {
      this.generateDateAxisRange(
        this.index,
        data.datasets.flatMap(set => (set.data.map(unit => (unit.indexDate())))),
      )
      this.data.datasets.forEach((dataset) => {
        dataset.data.forEach((dataUnit) => {
          dataUnit.intervalUnit = this.indexIntervalUnit
        })
      })
    }
    else {
      this.generateAxisRange(
        this.index,
        data.datasets.flatMap(set => (set.data.map(unit => (unit.index)))),
      )
    }
    this.index.posLine = [...this.index.pos]
    if (this.gridAlign) {
      this.index.min -= 0.5 * this.index.interval
      this.index.max += 0.5 * this.index.interval
      this.index.posLine.push(this.index.posLine[this.index.posLine.length - 1] + this.index.interval)
      this.index.posLine = this.index.posLine.map(pos => (pos - 0.5 * this.index.interval))
    }
    if (this.edgeOffset) {
      this.index.min -= 0.5 * this.index.interval
      this.index.max += 0.5 * this.index.interval
      this.index.posLine.unshift(-0.5 * this.index.interval)
      this.index.posLine.push(this.index.posLine[this.index.posLine.length - 1] + 0.5 * this.index.interval)
    }

    this.cross = {
      suggestedMin: options.axis?.cross?.suggestedMin
      ?? options.suggestedMin
      ?? (options.axis?.cross?.beginAtZero ?? true)
        ? 0
        : Number.MAX_VALUE,
      suggestedMax: options.axis?.cross?.suggestedMax
      ?? options.suggestedMax
      ?? (options.axis?.cross?.beginAtZero ?? true)
        ? Number.MIN_VALUE
        : 0,
      gridColor: options.axis?.cross?.gridColor ?? options.gridColor ?? Color.WHITE,
      gridWidth: options.axis?.cross?.gridWidth ?? options.gridWidth ?? 1,
      min: 0,
      max: 0,
      interval: 0,
      pos: [],
      ticks: [],
      grids: [],
      labels: [],
    }

    this.cross.axis = new Line(
      [0, this.size.height],
      [this.size.width, this.size.height],
      {
        style: {
          color: this.cross.gridColor,
          width: this.cross.gridWidth,
          transparency: 0.6,
        },
      },
    )

    this.generateAxisRange(
      this.cross,
      data.datasets.flatMap(set => (set.data.map(unit => (unit.cross)))),
    )

    if (this.indexAxis === 'x') {
      this.index.axis.from = [0, this.size.height]
      this.index.axis.to = [this.size.width, this.size.height]

      this.index.pos.forEach((pos, index) => {
        this.index.labels.push(
          new Text(
            this.index.labelTexts[index] ?? pos.toString(),
            {
              x: (pos - this.index.interval / 2 - this.index.min) / (this.index.max - this.index.min) * this.size.width,
              y: this.size.height + 4,
              width: this.index.interval / (this.index.max - this.index.min) * this.size.width,
              textAlign: 'center',
              style: {
                color: this.index.gridColor,
                fontSize: 16,
              },
            },
          ),
        )
      })

      this.index.posLine.forEach((pos) => {
        this.index.ticks.push(
          new Line(
            [(pos - this.index.min) / (this.index.max - this.index.min) * this.size.width, this.size.height],
            [(pos - this.index.min) / (this.index.max - this.index.min) * this.size.width, this.size.height + 5],
            {
              style: {
                color: this.index.gridColor,
                width: this.index.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
        this.index.grids.push(
          new Line(
            [(pos - this.index.min) / (this.index.max - this.index.min) * this.size.width, this.size.height],
            [(pos - this.index.min) / (this.index.max - this.index.min) * this.size.width, 0],
            {
              style: {
                color: this.index.gridColor,
                width: this.index.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
      })

      this.cross.axis.from = [0, this.size.height]
      this.cross.axis.to = [0, 0]

      for (let i = this.cross.min; i <= this.cross.max; i += this.cross.interval) {
        this.cross.ticks.push(
          new Line(
            [0, this.size.height - (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.height],
            [-5, this.size.height - (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.height],
            {
              style: {
                color: this.cross.gridColor,
                width: this.cross.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
        this.cross.labels.push(
          new Text(
            i.toString(),
            {
              x: -8 - stringWidth(i.toString()) * 12,
              y: this.size.height - 8 - (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.height,
              width: stringWidth(i.toString()) * 12,
              textAlign: 'right',
              style: {
                color: this.cross.gridColor,
                fontSize: 16,
              },
            },
          ),
        )
        this.cross.grids.push(
          new Line(
            [0, this.size.height - (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.height],
            [this.size.width, this.size.height - (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.height],
            {
              style: {
                color: this.cross.gridColor,
                width: this.cross.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
      }
    }
    else {
      this.index.axis.from = [0, this.size.height]
      this.index.axis.to = [0, 0]

      this.index.pos.forEach((pos, index) => {
        this.index.labels[index] = new Text(
          this.index.labelTexts[index] ?? pos.toString(),
          {
            x: -8 - stringWidth(this.index.labelTexts[index] ?? pos.toString()) * 12,
            y: this.size.height - 8 - (pos - this.index.min) / (this.index.max - this.index.min) * this.size.height,
            width: stringWidth(this.index.labelTexts[index] ?? pos.toString()) * 12,
            textAlign: 'right',
            style: {
              color: this.index.gridColor,
              fontSize: 16,
            },
          },
        )
      })

      this.index.posLine.forEach((pos) => {
        this.index.ticks.push(
          new Line(
            [0, this.size.height - (pos - this.index.min) / (this.index.max - this.index.min) * this.size.height],
            [-5, this.size.height - (pos - this.index.min) / (this.index.max - this.index.min) * this.size.height],
            {
              style: {
                color: this.index.gridColor,
                width: this.index.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
        this.index.grids.push(
          new Line(
            [0, this.size.height - (pos - this.index.min) / (this.index.max - this.index.min) * this.size.height],
            [this.size.width, this.size.height - (pos - this.index.min) / (this.index.max - this.index.min) * this.size.height],
            {
              style: {
                color: this.index.gridColor,
                width: this.index.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
      })

      this.cross.axis.from = [0, this.size.height]
      this.cross.axis.to = [this.size.width, this.size.height]

      for (let i = this.cross.min; i <= this.cross.max; i += this.cross.interval) {
        this.cross.ticks.push(
          new Line(
            [(i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.width, this.size.height],
            [(i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.width, this.size.height + 5],
            {
              style: {
                color: this.cross.gridColor,
                width: this.cross.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
        this.cross.labels.push(
          new Text(
            i.toString(),
            {
              x: (i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.width - stringWidth(i.toString()) * 6,
              y: this.size.height + 4,
              width: stringWidth(i.toString()) * 12,
              textAlign: 'center',
              style: {
                color: this.cross.gridColor,
                fontSize: 16,
              },
            },
          ),
        )
        this.cross.grids.push(
          new Line(
            [(i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.width, this.size.height],
            [(i - this.cross.min) / (this.cross.max - this.cross.min) * this.size.width, 0],
            {
              style: {
                color: this.cross.gridColor,
                width: this.cross.gridWidth,
                transparency: 0.3,
              },
            },
          ),
        )
      }
    }

    this.add(
      this.index.axis,
      ...this.index.ticks,
      ...this.index.labels,
      ...this.index.grids,
      this.cross.axis,
      ...this.cross.ticks,
      ...this.cross.labels,
      ...this.cross.grids,
    )

    this.legends = []
    this.legendLabels = []
    const legendWidthPrefix = [0]
    for (let i = 1; i <= this.data.datasets.length; i++) {
      legendWidthPrefix[i] = legendWidthPrefix[i - 1]
      legendWidthPrefix[i] += stringWidth(this.data.datasets[i - 1].label) * 12 + 36
    }
    for (let i = 0; i < this.data.datasets.length; i++) {
      this.legends.push(
        new Rect(
          20,
          16,
          {
            x: this.size.width / 2 - legendWidthPrefix[this.data.datasets.length] / 2 + legendWidthPrefix[i],
            y: -26,
            style: {
              fillColor: this.data.datasets[i].style.backgroundColor
              ?? this.data.datasets[i].data[0].style.backgroundColor ?? Color.WHITE,
              fillShader: this.data.datasets[i].style.backgroundShader
              ?? this.data.datasets[i].data[0].style.backgroundShader,
              borderColor: this.data.datasets[i].style.borderColor
              ?? this.data.datasets[i].data[0].style.borderColor ?? Color.WHITE,
              borderShader: this.data.datasets[i].style.borderShader
              ?? this.data.datasets[i].data[0].style.borderShader,
              borderWidth: this.data.datasets[i].style.borderWidth
              ?? this.data.datasets[i].data[0].style.borderWidth ?? 1,
              border: true,
            },
          },
        ),
      )
      this.legendLabels.push(
        new Text(
          this.data.datasets[i].label,
          {
            x: this.size.width / 2 - legendWidthPrefix[this.data.datasets.length] / 2 + legendWidthPrefix[i] + 24,
            y: -27,
            width: stringWidth(this.data.datasets[i].label) * 12,
            textAlign: 'left',
            style: {
              color: this.index.gridColor,
              fontSize: 16,
            },
          },
        ),
      )
    }

    this.add(
      ...this.legends,
      ...this.legendLabels,
    )
  }

  init(_ck: CanvasKit) {
    super.init(_ck)
  }

  draw(_canvas: Canvas) {
    super.draw(_canvas)
    this.index.axis.progress = this.progress
    this.index.grids.forEach(grid => (grid.progress = this.progress))
    this.index.ticks.forEach(tick => (tick.progress = this.progress))
    this.cross.axis.progress = this.progress
    this.cross.grids.forEach(grid => (grid.progress = this.progress))
    this.cross.ticks.forEach(tick => (tick.progress = this.progress))

    this.legendLabels.forEach(label => label.progress = Math.round(this.progress * 10) / 10)

    this.relayLegends()
  }

  /**
   * Generate the axis range and the positions of the axis labels.
   * @private
   * @param axis - The axis.
   * @param data - The data.
   */
  private generateAxisRange(axis: ChartAxis, data: number[]) {
    const minDataValue = Math.min(...data, typeof axis.suggestedMin === 'number' ? axis.suggestedMin : Number.NEGATIVE_INFINITY)
    const maxDataValue = Math.max(...data, typeof axis.suggestedMax === 'number' ? axis.suggestedMax : Number.POSITIVE_INFINITY)
    const range = maxDataValue - minDataValue
    const magnitude = Math.floor(Math.log10(range))
    axis.interval = 10 ** magnitude

    if (range / axis.interval < 5)
      axis.interval /= (Math.ceil(range / axis.interval) === 2 ? 4 : 2)

    axis.min = Math.floor(minDataValue / axis.interval) * axis.interval
    axis.max = Math.ceil(maxDataValue / axis.interval) * axis.interval

    for (let i = axis.min; i <= axis.max; i += axis.interval)
      axis.pos.push(i)
  }

  private generateDateAxisRange(axis: ChartAxis, data: DateTime[]) {
    const dateTypes: DateTimeUnit[] = ['year', 'month', 'week', 'day', 'hour', 'minute', 'second', 'millisecond']
    const minDataValue = DateTime.min(...data, axis.suggestedMin instanceof DateTime ? axis.suggestedMin : data[0])
    const maxDataValue = DateTime.max(...data, axis.suggestedMax instanceof DateTime ? axis.suggestedMax : data[0])
    let range = maxDataValue.diff(minDataValue, dateTypes).toObject()
    axis.interval = 1
    if (this.indexIntervalUnit === undefined) {
      for (const dateType of dateTypes) {
        if (range[`${dateType}s`] !== 0) {
          this.indexIntervalUnit = dateType
          break
        }
      }
    }
    while (dateTypes.shift() !== this.indexIntervalUnit) {
      // Do nothing.
    }
    dateTypes.unshift(this.indexIntervalUnit)

    let date = minDataValue.startOf(this.indexIntervalUnit)
    range = maxDataValue.diff(date, dateTypes).toObject()
    for (const dateType of dateTypes) {
      if (dateType !== this.indexIntervalUnit) {
        if (range[`${dateType}s`] > 0) {
          range[`${this.indexIntervalUnit}s`]++
          break
        }
      }
    }
    // if (range[`${this.indexIntervalUnit}s`] < 5)
    //   axis.interval /= (Math.ceil(range[`${this.indexIntervalUnit}s`]) === 2 ? 4 : 2)
    axis.min = minDataValue.startOf(this.indexIntervalUnit)
      .get(this.indexIntervalUnit !== 'week' ? this.indexIntervalUnit : 'weekYear')
    axis.max = axis.min + range[`${this.indexIntervalUnit}s`]

    for (let i = axis.min; i <= axis.max; i += axis.interval) {
      axis.pos.push(i)
      axis.labelTexts.push(date[this.dateFormatOptions[this.indexIntervalUnit]].toString())
      date = date.plus({ [this.indexIntervalUnit]: axis.interval })
    }
  }

  private relayLegends() {
    const legendWidthPrefix = [0]
    for (let i = 1; i <= this.legendLabels.length; i++) {
      legendWidthPrefix[i] = legendWidthPrefix[i - 1]
      legendWidthPrefix[i] += this.legendLabels[i - 1].getLineMetrics()[0].width + 36
    }
    legendWidthPrefix[legendWidthPrefix.length - 1] -= 6
    for (let i = 0; i < this.data.datasets.length; i++) {
      this.legends[i].x = this.size.width / 2 - legendWidthPrefix[this.data.datasets.length] / 2 + legendWidthPrefix[i]
      this.legendLabels[i].x = this.size.width / 2 - legendWidthPrefix[this.data.datasets.length] / 2 + legendWidthPrefix[i] + 28
    }
  }
}
